using System;
using System.Web;

namespace Pegasus.Web.Security.SecureWebPage
{

    /// <summary>
    /// Provides static methods for ensuring that a page is rendered 
    /// securely via SSL or unsecurely.
    /// </summary>
    public sealed class SSLHelper
    {

        // Protocol prefixes.
        private const string UnsecureProtocolPrefix = "http://";
        private const string SecureProtocolPrefix = "https://";

        /// <summary>
        /// Prevent creating an instance of this class.
        /// </summary>
        private SSLHelper()
        {
        }

        /// <summary>
        /// Determines the secure page that should be requested if a redirect occurs.
        /// </summary>
        /// <param name="settings">The SecureWebPageSettings to use in determining.</param>
        /// <param name="ignoreCurrentProtocol">
        /// A flag indicating whether or not to ingore the current protocol when determining.
        /// </param>
        /// <returns>A string containing the absolute URL of the secure page to redirect to.</returns>
        public static string DetermineSecurePage(SecureWebPageSettings settings, bool ignoreCurrentProtocol)
        {
            string Result = null;

            // Is this request already secure?
            string RequestPath = HttpContext.Current.Request.Url.AbsoluteUri;
            if (ignoreCurrentProtocol || RequestPath.StartsWith(UnsecureProtocolPrefix))
            {
                SecureWebPageDomainSetting domain = settings.GetCurrentDomainMapping();

                // Is there a different URI to redirect to?
                if (domain == null || domain.EncryptedUri == null || domain.EncryptedUri.Length == 0)
                {
                    // Replace the protocol of the requested URL with "https".
                    Result = RequestPath.Replace(UnsecureProtocolPrefix, SecureProtocolPrefix);
                }
                else
                {
                    // Build the URL with the "https" protocol.
                    Result = BuildUrl(true, settings.MaintainPath, domain.EncryptedUri, domain.UnencryptedUri);
                }
            }

            return Result;
        }

		/// <summary>
		/// Determines the unsecure page that should be requested if a redirect occurs.
		/// </summary>
		/// <param name="settings">The SecureWebPageSettings to use in determining.</param>
		/// <param name="ignoreCurrentProtocol">A flag indicating whether or not to ingore the current protocol when determining.</param>
		/// <param name="cookieDomain">The cookie domain.</param>
		/// <returns>
		/// A string containing the absolute URL of the unsecure page to redirect to.
		/// </returns>
        public static string DetermineUnsecurePage(SecureWebPageSettings settings, bool ignoreCurrentProtocol, out String cookieDomain)
        {
            string Result = null;

            cookieDomain = String.Empty;

            // Is this request secure?
            string RequestPath = HttpContext.Current.Request.Url.AbsoluteUri;
            if (ignoreCurrentProtocol || RequestPath.StartsWith(SecureProtocolPrefix))
            {
                // Get the current Uri Mapping from the settings.
                SecureWebPageDomainSetting domain = settings.GetCurrentDomainMapping();

                // Is there a different URI to redirect to?
                if (domain == null || domain.UnencryptedUri == null || domain.UnencryptedUri.Length == 0)
                {
                    // Replace the protocol of the requested URL with "http".
                    Result = RequestPath.Replace(SecureProtocolPrefix, UnsecureProtocolPrefix);
                }
                else
                {
                    // Build the URL with the "http" protocol.
                    Result = BuildUrl(false, settings.MaintainPath, domain.EncryptedUri, domain.UnencryptedUri);
                    cookieDomain = domain.UnencryptedUri;
                }
            }

            return Result;
        }

        /// <summary>
        /// Requests the current page over a secure connection, if it is not already.
        /// </summary>
        /// <param name="settings">The SecureWebPageSettings to use for this request.</param>
        public static void RequestSecurePage(SecureWebPageSettings settings)
        {
            // Determine the response path, if any.
            string ResponsePath = DetermineSecurePage(settings, false);
            if (ResponsePath != null && ResponsePath != string.Empty)
            {
                // Redirect to the secure page.
                HttpContext.Current.Response.Redirect(ResponsePath, true);
            }
        }

        /// <summary>
        /// Requests the current page over an unsecure connection, if it is not already.
        /// </summary>
        /// <param name="settings">The SecureWebPageSettings to use for this request.</param>
        public static void RequestUnsecurePage(SecureWebPageSettings settings)
        {
            String cookieDomain = String.Empty;

            // Determine the response path, if any.
            string ResponsePath = DetermineUnsecurePage(settings, false, out cookieDomain);
            if (ResponsePath != null && ResponsePath != string.Empty)
            {
                HttpRequest Request = HttpContext.Current.Request;

                // Test for the need to bypass a security warning.
                bool Bypass;
                if (settings.WarningBypassMode == SecurityWarningBypassMode.AlwaysBypass)
                    Bypass = true;
                else if (settings.WarningBypassMode == SecurityWarningBypassMode.BypassWithQueryParam &&
                        Request.QueryString[settings.BypassQueryParamName] != null)
                {
                    Bypass = true;

                    // Remove the bypass query parameter from the URL.
                    System.Text.StringBuilder NewPath = new System.Text.StringBuilder(ResponsePath);
                    int i = ResponsePath.LastIndexOf(string.Format("?{0}=", settings.BypassQueryParamName));
                    if (i < 0)
                        i = ResponsePath.LastIndexOf(string.Format("&{0}=", settings.BypassQueryParamName));
                    NewPath.Remove(i, settings.BypassQueryParamName.Length + Request.QueryString[settings.BypassQueryParamName].Length + 1);

                    // Remove any abandoned "&" character.
                    if (i >= NewPath.Length)
                        i = NewPath.Length - 1;
                    if (NewPath[i] == '&')
                        NewPath.Remove(i, 1);

                    // Remove any abandoned "?" character.
                    i = NewPath.Length - 1;
                    if (NewPath[i] == '?')
                        NewPath.Remove(i, 1);

                    ResponsePath = NewPath.ToString();
                }
                else
                    Bypass = false;

                // Output a redirector for the needed page to avoid a security warning.
                HttpResponse Response = HttpContext.Current.Response;
                if (Bypass)
                {
                    // Clear the current response.
                    Response.Clear();

                    // Add a refresh header to the response for the new path.
                    Response.AddHeader("Refresh", string.Concat("0;URL=", ResponsePath));

                    // Also, add JavaScript to replace the current location as backup.
                    Response.Write("<html><head><title></title>");
                    Response.Write("<!-- <script language=\"javascript\">window.location.replace(\"");
                    Response.Write(ResponsePath);
                    Response.Write("\");</script> -->");
                    Response.Write("</head><body></body></html>");

                    Response.End();
                }
                else
                    // Redirect to the insecure page.
                    Response.Redirect(ResponsePath, true);
            }
        }

        /// <summary>
        /// Ensures that the cookies on the response are setup so that they will end up being visible to
        /// both the secure and nonsecure domains.
        /// </summary>
        /// <param name="settings">the settings.</param>
        public static void EnsureCookieVisibility(
            SecureWebPageSettings settings)
        {
            // No settings? Bail.
            if (settings == null)
                return;

            // Get the current domain mapping so that we can determine the proper cookie domain.
            SecureWebPageDomainSetting domain = settings.GetCurrentDomainMapping();

            // No domain setting? Bail.
            if (domain == null)
                return;

            // No cookie domain? Bail.
            if (domain.CookieDomain == null || domain.CookieDomain == String.Empty)
                return;

            // Go through every cookie in the response. These contain all cookie modifications
            // From the request. Set them up so that the have the right cookie domain.
            for (int i = 0; i < HttpContext.Current.Response.Cookies.Count; i++)
            {
                // Get the cookie.
                HttpCookie cook = HttpContext.Current.Response.Cookies[i];

                // If the cookie domain is not set yet to the proper setting,
                // then set it. Otherwise ignore it.
                if (cook.Domain != domain.CookieDomain)
                {
                    cook.Domain = domain.CookieDomain;
                }
            }
        }

        /// <summary>
        /// Builds a URL from the given protocol and appropriate host path. The resulting URL 
        /// will maintain the current path if requested.
        /// </summary>
        /// <param name="secure">Is this to be a secure URL?</param>
        /// <param name="maintainPath">Should the current path be maintained during transfer?</param>
        /// <param name="encryptedUri">The URI to redirect to for encrypted requests.</param>
        /// <param name="unencryptedUri">The URI to redirect to for standard requests.</param>
        /// <returns></returns>
        private static string BuildUrl(bool secure, bool maintainPath, string encryptedUri, string unencryptedUri)
        {
            // Clean the URIs.
            encryptedUri = CleanHostUri(encryptedUri);
            unencryptedUri = CleanHostUri(unencryptedUri);

            // Get the current request.
            HttpRequest Request = HttpContext.Current.Request;

            // Prepare to build the needed URL.
            System.Text.StringBuilder Url = new System.Text.StringBuilder();

            // Host authority (e.g. secure.mysite.com/).
            if (secure)
                Url.Append(encryptedUri);
            else
                Url.Append(unencryptedUri);

            if (maintainPath)
                // Append the current file path.
                Url.Append(Request.CurrentExecutionFilePath).Append(Request.Url.Query);
            else
            {
                // Append just the current page
                string CurrentUrl = Request.Url.AbsolutePath;
                Url.Append(CurrentUrl.Substring(CurrentUrl.LastIndexOf('/') + 1)).Append(Request.Url.Query);
            }

            // Replace any double slashes with a single slash.
            Url.Replace("//", "/");

            // Prepend the protocol.
            if (secure)
                Url.Insert(0, SecureProtocolPrefix);
            else
                Url.Insert(0, UnsecureProtocolPrefix);

            return Url.ToString();
        }

        /// <summary>
        /// Cleans a host path by stripping out any unneeded elements.
        /// </summary>
        /// <param name="uri">The host URI to validate.</param>
        /// <returns>Returns a string that is stripped as needed.</returns>
        private static string CleanHostUri(string uri)
        {
            Uri HostUri = ConvertToUri(uri);
            string Result = string.Concat(HostUri.Authority, HostUri.AbsolutePath);
            if (!Result.EndsWith("/"))
                Result += "/";

            return Result;
        }

        private static Uri ConvertToUri(
            string uri)
        {
            return ConvertToUri(false, uri);
        }

        private static Uri ConvertToUri(
            bool secure,
            string uri)
        {
            if (String.IsNullOrEmpty(uri)) return null;

            // Ensure there is a protocol or a Uri cannot be constructed.
            if (!uri.StartsWith(UnsecureProtocolPrefix) && !uri.StartsWith(SecureProtocolPrefix))
            {
                if (secure)
                {
                    uri = SecureProtocolPrefix + uri;
                }
                else
                {
                    uri = UnsecureProtocolPrefix + uri;
                }
            }

            // Extract the authority and path to build a string suitable for our needs.
            return new Uri(uri);
        }
    }

}
